{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 도구 (Tools)\n",
    "\n",
    "도구(Tool)는 에이전트, 체인 또는 LLM이 외부 세계와 상호작용하기 위한 인터페이스입니다.\n",
    "\n",
    "LangChain 에서 기본 제공하는 도구를 사용하여 쉽게 도구를 활용할 수 있으며, 사용자 정의 도구(Custom Tool) 를 쉽게 구축하는 것도 가능합니다.\n",
    "\n",
    "**LangChain 에 통합된 도구 리스트는 아래 링크에서 확인할 수 있습니다.**\n",
    "\n",
    "- [LangChain 통합된 도구 리스트](https://python.langchain.com/v0.1/docs/integrations/tools/)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# API 키를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API 키 정보 로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install -qU langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH15-Tools\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "import warnings\n",
    "\n",
    "# 경고 메시지 무시\n",
    "warnings.filterwarnings(\"ignore\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 빌트인 도구(built-in tools)\n",
    "\n",
    "랭체인에서 제공하는 사전에 정의된 도구(tool) 와 툴킷(toolkit) 을 사용할 수 있습니다.\n",
    "\n",
    "tool 은 단일 도구를 의미하며, toolkit 은 여러 도구를 묶어서 하나의 도구로 사용할 수 있습니다.\n",
    "\n",
    "관련 도구는 아래의 링크에서 참고하실 수 있습니다.\n",
    "\n",
    "**참고**\n",
    "- [LangChain Tools/Toolkits](https://python.langchain.com/docs/integrations/tools/)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Python REPL 도구\n",
    "\n",
    "이 도구는 Python 코드를 REPL(Read-Eval-Print Loop) 환경에서 실행하기 위한 클래스를 제공합니다\n",
    "- PythonREPLTool\n",
    "\n",
    "**설명**\n",
    "\n",
    "- Python 셸 환경을 제공합니다.\n",
    "- 유효한 Python 명령어를 입력으로 받아 실행합니다.\n",
    "- 결과를 보려면 print(...) 함수를 사용해야 합니다.\n",
    "\n",
    "**주요 특징**\n",
    "\n",
    "- sanitize_input: 입력을 정제하는 옵션 (기본값: True)\n",
    "- python_repl: PythonREPL 인스턴스 (기본값: 전역 범위에서 실행)\n",
    "\n",
    "**사용 방법**\n",
    "\n",
    "- PythonREPLTool 인스턴스 생성\n",
    "- run 또는 arun, invoke 메서드를 사용하여 Python 코드 실행\n",
    "\n",
    "**입력 정제**\n",
    "\n",
    "- 입력 문자열에서 불필요한 공백, 백틱, 'python' 키워드 등을 제거합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_experimental.tools import PythonREPLTool\n",
    "\n",
    "# 파이썬 코드를 실행하는 도구를 생성합니다.\n",
    "python_tool = PythonREPLTool()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 파이썬 코드를 실행하고 결과를 반환합니다.\n",
    "print(python_tool.invoke(\"print(100 + 200)\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "아래는 LLM 에게 파이썬 코드를 작성하도록 요청하고 결과를 반환하는 예제입니다.\n",
    "\n",
    "**흐름 정리**\n",
    "1. LLM 모델에게 특정 작업을 수행하는 Python 코드를 작성하도록 요청합니다.\n",
    "2. 작성된 코드를 실행하여 결과를 얻습니다.\n",
    "3. 결과를 출력합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "from langchain_core.runnables import RunnableLambda\n",
    "\n",
    "\n",
    "# 파이썬 코드를 실행하고 중간 과정을 출력하고 도구 실행 결과를 반환하는 함수\n",
    "def print_and_execute(code, debug=False):\n",
    "    if debug:\n",
    "        print(\"CODE:\")\n",
    "        print(code)\n",
    "    return python_tool.invoke(code)\n",
    "\n",
    "\n",
    "# 파이썬 코드를 작성하도록 요청하는 프롬프트\n",
    "prompt = ChatPromptTemplate.from_messages(\n",
    "    [\n",
    "        (\n",
    "            \"system\",\n",
    "            \"You are Raymond Hetting, an expert python programmer, well versed in meta-programming and elegant, concise and short but well documented code. You follow the PEP8 style guide. \"\n",
    "            \"Return only the code, no intro, no explanation, no chatty, no markdown, no code block, no nothing. Just the code.\",\n",
    "        ),\n",
    "        (\"human\", \"{input}\"),\n",
    "    ]\n",
    ")\n",
    "# LLM 모델 생성\n",
    "llm = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n",
    "\n",
    "# 프롬프트와 LLM 모델을 사용하여 체인 생성\n",
    "chain = prompt | llm | StrOutputParser() | RunnableLambda(print_and_execute)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 결과 출력\n",
    "print(chain.invoke(\"로또 번호 생성기를 출력하는 코드를 작성하세요.\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 검색 API 도구\n",
    "\n",
    "Tavily 검색 API를 활용하여 검색 기능을 구현하는 도구입니다. 이 도구는 두 가지 주요 클래스를 제공합니다: `TavilySearchResults`와 `TavilyAnswer`.\n",
    "\n",
    "**API 키 발급 주소**\n",
    "- https://app.tavily.com/\n",
    "\n",
    "발급한 API 키를 환경변수에 설정합니다.\n",
    "\n",
    "`.env` 파일에 아래와 같이 설정합니다.\n",
    "\n",
    "```\n",
    "TAVILY_API_KEY=tvly-abcdefghijklmnopqrstuvwxyz\n",
    "```\n",
    "\n",
    "### TavilySearchResults\n",
    "\n",
    "**설명**\n",
    "- Tavily 검색 API를 쿼리하고 JSON 형식의 결과를 반환합니다.\n",
    "- 포괄적이고 정확하며 신뢰할 수 있는 결과에 최적화된 검색 엔진입니다.\n",
    "- 현재 이벤트에 대한 질문에 답변할 때 유용합니다.\n",
    "\n",
    "**주요 매개변수**\n",
    "- `max_results` (int): 반환할 최대 검색 결과 수 (기본값: 5)\n",
    "- `search_depth` (str): 검색 깊이 (\"basic\" 또는 \"advanced\")\n",
    "- `include_domains` (List[str]): 검색 결과에 포함할 도메인 목록\n",
    "- `exclude_domains` (List[str]): 검색 결과에서 제외할 도메인 목록\n",
    "- `include_answer` (bool): 원본 쿼리에 대한 짧은 답변 포함 여부\n",
    "- `include_raw_content` (bool): 각 사이트의 정제된 HTML 콘텐츠 포함 여부\n",
    "- `include_images` (bool): 쿼리 관련 이미지 목록 포함 여부\n",
    "\n",
    "**반환 값**\n",
    "- 검색 결과를 포함하는 JSON 형식의 문자열(url, content)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "혹은 아래의 주석을 해제하고 발급받은 API 키를 입력합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "# import os\n",
    "\n",
    "# os.environ[\"TAVILY_API_KEY\"] = \"TAVILY API 키 입력\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.tools.tavily_search import TavilySearchResults\n",
    "\n",
    "# 도구 생성\n",
    "tool = TavilySearchResults(\n",
    "    max_results=6,\n",
    "    include_answer=True,\n",
    "    include_raw_content=True,\n",
    "    # include_images=True,\n",
    "    # search_depth=\"advanced\", # or \"basic\"\n",
    "    include_domains=[\"github.io\", \"wikidocs.net\"],\n",
    "    # exclude_domains = []\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 도구 실행\n",
    "tool.invoke({\"query\": \"LangChain Tools 에 대해서 알려주세요\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Image 생성 도구 (DALL-E)\n",
    "\n",
    "- `DallEAPIWrapper 클래스`: OpenAI의 DALL-E 이미지 생성기를 위한 래퍼(wrapper)입니다.\n",
    "\n",
    "이 도구를 사용하면 DALL-E API를 쉽게 통합하여 텍스트 기반 이미지 생성 기능을 구현할 수 있습니다. 다양한 설정 옵션을 통해 유연하고 강력한 이미지 생성 도구로 활용할 수 있습니다.\n",
    "\n",
    "**주요 속성**\n",
    "\n",
    "- `model`: 사용할 DALL-E 모델 이름 (기본값: \"dall-e-2\", \"dall-e-3\")\n",
    "\n",
    "- `n`: 생성할 이미지 수 (기본값: 1)\n",
    "\n",
    "- `size`: 생성할 이미지 크기\n",
    "  - \"dall-e-2\": \"1024x1024\", \"512x512\", \"256x256\"\n",
    "  - \"dall-e-3\": \"1024x1024\", \"1792x1024\", \"1024x1792\"\n",
    "\n",
    "- `style`: 생성될 이미지의 스타일 (기본값: \"natural\", \"vivid\")\n",
    "\n",
    "- `quality`: 생성될 이미지의 품질 (기본값: \"standard\", \"hd\")\n",
    "\n",
    "- `max_retries`: 생성 시 최대 재시도 횟수\n",
    "\n",
    "**주요 기능**\n",
    "- DALL-E API를 사용하여 텍스트 설명에 기반한 이미지 생성"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**흐름 정리**\n",
    "\n",
    "다음은 DALL-E Image Generator 를 사용하여 이미지를 생성하는 예제입니다.\n",
    "\n",
    "이번에는 `DallEAPIWrapper` 를 사용하여 이미지를 생성해 보겠습니다.\n",
    "\n",
    "이때 입력 프롬프트는 LLM 모델에게 이미지를 생성하는 프롬프트를 작성하도록 요청합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.output_parsers import StrOutputParser\n",
    "from langchain_core.prompts import PromptTemplate\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "# ChatOpenAI 모델 초기화\n",
    "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0.9, max_tokens=1000)\n",
    "\n",
    "# DALL-E 이미지 생성을 위한 프롬프트 템플릿 정의\n",
    "prompt = PromptTemplate.from_template(\n",
    "    \"Generate a detailed IMAGE GENERATION prompt for DALL-E based on the following description. \"\n",
    "    \"Return only the prompt, no intro, no explanation, no chatty, no markdown, no code block, no nothing. Just the prompt\"\n",
    "    \"Output should be less than 1000 characters. Write in English only.\"\n",
    "    \"Image Description: \\n{image_desc}\",\n",
    ")\n",
    "\n",
    "# 프롬프트, LLM, 출력 파서를 연결하는 체인 생성\n",
    "chain = prompt | llm | StrOutputParser()\n",
    "\n",
    "# 체인 실행\n",
    "image_prompt = chain.invoke(\n",
    "    {\"image_desc\": \"스마트폰을 바라보는 사람들을 풍자한 neo-classicism painting\"}\n",
    ")\n",
    "\n",
    "# 이미지 프롬프트 출력\n",
    "print(image_prompt)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "그럼, 이전에 생성한 이미지 프롬프트를 `DallEAPIWrapper` 에 입력하여 이미지를 생성해 보겠습니다."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**`DallEAPIWrapper` 에 대한 임시 버그 안내사항** (작성일: 2024-10-13)\n",
    "\n",
    "- 현재 langchain 0.3.x 이상 버전에서 `DallEAPIWrapper` 에 대한 임시 버그가 있습니다. (`401 오류: invalid API key`)\n",
    "\n",
    "따라서, 아래의 코드를 오류 없이 실행하기 위해서는 LangChain 버전을 0.2.16 으로 변경해야 합니다.\n",
    "\n",
    "아래의 주석을 해제하고 실행하면 LangChain 버전을 0.2.16 으로 변경됩니다.\n",
    "\n",
    "하지만, 이후 내용에서는 LangChain 버전을 0.3.x 이상으로 변경하여 사용하기 때문에\n",
    "\n",
    "`poetry shell` 명령어를 통해 다시 최신 langchain 버전으로 변경해야 합니다.\n",
    "\n",
    "이 과정이 번거로운 분들은 일단 `DallEAPIWrapper` 를 사용하지 않고 진행하셔도 무방합니다.\n",
    "\n",
    "**업그레이드/다운그레이드** 후에는 반드시 상단 메뉴의 \"Restart\" 버튼을 클릭한 뒤 진행해야 합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 임시 버전 다운그레이드 명령어 (실행 후 restart)\n",
    "# !pip install langchain==0.2.16 langchain-community==0.2.16 langchain-text-splitters==0.2.4 langchain-experimental==0.0.65 langchain-openai==0.1.20"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'chain' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mNameError\u001b[0m                                 Traceback (most recent call last)",
      "Cell \u001b[0;32mIn[1], line 24\u001b[0m\n\u001b[1;32m     19\u001b[0m query \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m스마트폰을 바라보는 사람들을 풍자한 neo-classicism painting\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m     21\u001b[0m \u001b[38;5;66;03m# 이미지 생성 및 URL 받기\u001b[39;00m\n\u001b[1;32m     22\u001b[0m \u001b[38;5;66;03m# chain.invoke()를 사용하여 이미지 설명을 DALL-E 프롬프트로 변환\u001b[39;00m\n\u001b[1;32m     23\u001b[0m \u001b[38;5;66;03m# dalle.run()을 사용하여 실제 이미지 생성\u001b[39;00m\n\u001b[0;32m---> 24\u001b[0m image_url \u001b[38;5;241m=\u001b[39m dalle\u001b[38;5;241m.\u001b[39mrun(\u001b[43mchain\u001b[49m\u001b[38;5;241m.\u001b[39minvoke({\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mimage_desc\u001b[39m\u001b[38;5;124m\"\u001b[39m: query}))\n\u001b[1;32m     26\u001b[0m \u001b[38;5;66;03m# 생성된 이미지를 표시합니다.\u001b[39;00m\n\u001b[1;32m     27\u001b[0m Image(url\u001b[38;5;241m=\u001b[39mimage_url, width\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m500\u001b[39m)\n",
      "\u001b[0;31mNameError\u001b[0m: name 'chain' is not defined"
     ]
    }
   ],
   "source": [
    "# DALL-E API 래퍼 가져오기\n",
    "from langchain_community.utilities.dalle_image_generator import DallEAPIWrapper\n",
    "from IPython.display import Image\n",
    "import os\n",
    "\n",
    "# DALL-E API 래퍼 초기화\n",
    "# model: 사용할 DALL-E 모델 버전\n",
    "# size: 생성할 이미지 크기\n",
    "# quality: 이미지 품질\n",
    "# n: 생성할 이미지 수\n",
    "dalle = DallEAPIWrapper(\n",
    "    model=\"dall-e-3\",\n",
    "    size=\"1024x1024\",\n",
    "    quality=\"standard\",\n",
    "    n=1,\n",
    ")\n",
    "\n",
    "# 질문\n",
    "query = \"스마트폰을 바라보는 사람들을 풍자한 neo-classicism painting\"\n",
    "\n",
    "# 이미지 생성 및 URL 받기\n",
    "# chain.invoke()를 사용하여 이미지 설명을 DALL-E 프롬프트로 변환\n",
    "# dalle.run()을 사용하여 실제 이미지 생성\n",
    "image_url = dalle.run(chain.invoke({\"image_desc\": query}))\n",
    "\n",
    "# 생성된 이미지를 표시합니다.\n",
    "Image(url=image_url, width=500)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 사용자 정의 도구(Custom Tool)\n",
    "\n",
    "LangChain 에서 제공하는 빌트인 도구 외에도 사용자가 직접 도구를 정의하여 사용할 수 있습니다.\n",
    "\n",
    "이를 위해서는 `langchain.tools` 모듈에서 제공하는 `tool` 데코레이터를 사용하여 함수를 도구로 변환합니다.\n",
    "\n",
    "### @tool 데코레이터\n",
    "\n",
    "이 데코레이터는 함수를 도구로 변환하는 기능을 제공합니다. 다양한 옵션을 통해 도구의 동작을 커스터마이즈할 수 있습니다.\n",
    "\n",
    "**사용 방법**\n",
    "1. 함수 위에 `@tool` 데코레이터 적용\n",
    "2. 필요에 따라 데코레이터 매개변수 설정\n",
    "\n",
    "이 데코레이터를 사용하면 일반 Python 함수를 강력한 도구로 쉽게 변환할 수 있으며, 자동화된 문서화와 유연한 인터페이스 생성이 가능합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.tools import tool\n",
    "\n",
    "\n",
    "# 데코레이터를 사용하여 함수를 도구로 변환합니다.\n",
    "@tool\n",
    "def add_numbers(a: int, b: int) -> int:\n",
    "    \"\"\"Add two numbers\"\"\"\n",
    "    return a + b\n",
    "\n",
    "\n",
    "@tool\n",
    "def multiply_numbers(a: int, b: int) -> int:\n",
    "    \"\"\"Multiply two numbers\"\"\"\n",
    "    return a * b"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 도구 실행\n",
    "add_numbers.invoke({\"a\": 3, \"b\": 4})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 도구 실행\n",
    "multiply_numbers.invoke({\"a\": 3, \"b\": 4})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 구글 뉴스기사 검색 도구\n",
    "\n",
    "`langchain-teddynote` 패키지에서 제공하는 `GoogleNews` 도구를 사용하여 구글 뉴스기사를 검색하는 도구입니다.\n",
    "\n",
    "**참고**\n",
    "- API 키가 필요하지 않습니다. (RSS 피드를 사용하기 때문)\n",
    "\n",
    "news.google.com 에서 제공하는 뉴스기사를 검색하는 도구입니다.\n",
    "\n",
    "**설명**\n",
    "- 구글 뉴스 검색 API를 사용하여 최신 뉴스를 검색합니다.\n",
    "- 키워드를 기반으로 뉴스를 검색할 수 있습니다.\n",
    "- 최신 뉴스를 검색할 수 있습니다.\n",
    "\n",
    "**주요 매개변수**\n",
    "- `k` (int): 반환할 최대 검색 결과 수 (기본값: 5)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "사용하기 전 패키지를 업데이트 해주세요."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "# !pip install -qU langchain-teddynote"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_teddynote.tools import GoogleNews\n",
    "\n",
    "# 도구 생성\n",
    "news_tool = GoogleNews()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 최신 뉴스 검색\n",
    "news_tool.search_latest(k=5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 키워드로 뉴스 검색\n",
    "news_tool.search_by_keyword(\"AI 투자\", k=3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_teddynote.tools import GoogleNews\n",
    "from langchain.tools import tool\n",
    "from typing import List, Dict\n",
    "\n",
    "\n",
    "# 키워드로 뉴스 검색하는 도구 생성\n",
    "@tool\n",
    "def search_keyword(query: str) -> List[Dict[str, str]]:\n",
    "    \"\"\"Look up news by keyword\"\"\"\n",
    "    print(query)\n",
    "    news_tool = GoogleNews()\n",
    "    return news_tool.search_by_keyword(query, k=5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 실행 결과\n",
    "search_keyword.invoke({\"query\": \"LangChain AI\"})"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "langchain-kr-lwwSZlnu-py3.11",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
